bookwiz.io / app / api / books / [id] / github-integration / commit / route.ts
route.ts
Raw
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

// Create server-side Supabase client with user session
function createServerSupabaseClient(request: Request) {
  // Get the authorization header from the request
  const authHeader = request.headers.get('authorization')
  
  return createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      auth: {
        autoRefreshToken: false,
        persistSession: false
      },
      global: {
        headers: authHeader ? {
          Authorization: authHeader
        } : {}
      }
    }
  )
}

export async function POST(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const supabase = createServerSupabaseClient(request)
    const bookId = params.id
    const { message, changes } = await request.json()

    if (!message || !changes || changes.length === 0) {
      return NextResponse.json(
        { error: 'Commit message and changes are required' },
        { status: 400 }
      )
    }

    // Get current user
    const { data: { user }, error: authError } = await supabase.auth.getUser()
    if (authError || !user) {
      return NextResponse.json(
        { error: 'Authentication required' },
        { status: 401 }
      )
    }

    // Get GitHub integration
    const { data: profile } = await supabase
      .from('profiles')
      .select('github_integrations')
      .eq('id', user.id)
      .single()

    const integration = profile?.github_integrations?.[bookId]
    if (!integration) {
      return NextResponse.json(
        { error: 'GitHub integration not found' },
        { status: 404 }
      )
    }

    // Get book files to create file content for commit
    const { data: files } = await supabase
      .from('file_system_items')
      .select('*')
      .eq('book_id', bookId)

    if (!files) {
      return NextResponse.json(
        { error: 'No files found' },
        { status: 404 }
      )
    }

    // Create file contents for GitHub commit
    const fileContents: { [path: string]: string } = {}
    
    // Build file structure
    const buildFilePath = (fileId: string, allFiles: any[]): string => {
      const file = allFiles.find(f => f.id === fileId)
      if (!file) return ''
      
      // For files, ensure extension is included
      let fileName = file.name
      if (file.type === 'file' && file.file_extension && !fileName.includes('.')) {
        fileName = `${fileName}.${file.file_extension}`
      }
      
      if (!file.parent_id) return fileName
      
      const parentPath = buildFilePath(file.parent_id, allFiles)
      return parentPath ? `${parentPath}/${fileName}` : fileName
    }

    // Add all files to commit
    files.forEach(file => {
      if (file.type === 'file') {
        const filePath = buildFilePath(file.id, files)
        fileContents[filePath] = file.content || ''
      }
    })

    // Commit via GitHub API
    const owner = integration.github_username
    const repo = integration.repository_name
    const accessToken = integration.access_token

    // Get current branch reference
    const branchResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Accept': 'application/vnd.github.v3+json'
      }
    })

    if (!branchResponse.ok) {
      throw new Error('Failed to get branch reference')
    }

    const branchData = await branchResponse.json()
    const parentSha = branchData.object.sha

    // Get current tree
    const treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${parentSha}`, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Accept': 'application/vnd.github.v3+json'
      }
    })

    if (!treeResponse.ok) {
      throw new Error('Failed to get current tree')
    }

    // Create blobs and tree entries
    const treeEntries = await Promise.all(
      Object.entries(fileContents).map(async ([path, content]) => {
        const blobResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/blobs`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Accept': 'application/vnd.github.v3+json',
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            content: Buffer.from(content).toString('base64'),
            encoding: 'base64'
          })
        })

        if (!blobResponse.ok) {
          throw new Error(`Failed to create blob for ${path}`)
        }

        const blob = await blobResponse.json()
        return {
          path,
          mode: '100644',
          type: 'blob',
          sha: blob.sha
        }
      })
    )

    // Create new tree
    const newTreeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Accept': 'application/vnd.github.v3+json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        base_tree: parentSha,
        tree: treeEntries
      })
    })

    if (!newTreeResponse.ok) {
      throw new Error('Failed to create tree')
    }

    const newTree = await newTreeResponse.json()

    // Create commit
    const commitResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Accept': 'application/vnd.github.v3+json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        message,
        tree: newTree.sha,
        parents: [parentSha],
        author: {
          name: user.user_metadata?.full_name || user.email?.split('@')[0] || 'BookWiz User',
          email: user.email,
          date: new Date().toISOString()
        }
      })
    })

    if (!commitResponse.ok) {
      throw new Error('Failed to create commit')
    }

    const commit = await commitResponse.json()

    // Update branch reference
    const updateRefResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`, {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Accept': 'application/vnd.github.v3+json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        sha: commit.sha
      })
    })

    if (!updateRefResponse.ok) {
      throw new Error('Failed to update branch reference')
    }

    return NextResponse.json({
      success: true,
      commit: {
        sha: commit.sha,
        message: commit.message,
        url: commit.html_url
      }
    })

  } catch (error) {
    return NextResponse.json(
      { error: error instanceof Error ? error.message : 'Failed to commit changes' },
      { status: 500 }
    )
  }
}